﻿namespace Refunctor

open System
open System.Text
open Reflector
open Reflector.CodeModel
open Reflector.CodeModel.Memory
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Data
open System.Windows.Media
open System.Windows.Media.Imaging
open Microsoft.Win32
open System.IO
open ICSharpCode.AvalonEdit
open System.Diagnostics
open System.Collections.Concurrent
open ICSharpCode.AvalonEdit.Document
open ICSharpCode.AvalonEdit.Editing
open System.Collections.Generic
open System.Text.RegularExpressions
open System.Reflection

type CommandProcessStatus =
    | Success = 0
    | Error = 1
    | InProgess = 2

[<Sealed>]
[<AllowNullLiteral>]
type internal InteractiveConsole() as self =
    inherit ContentControl()
    
    [<DefaultValue>]    
    val mutable editor : ICSharpCode.AvalonEdit.TextEditor
    
    [<DefaultValue>]
    val mutable toolbar : ToolBar

    let fsiProcess = new Process()
    let outputQueue = new ConcurrentQueue<string>()   

    let mutable cleared = false
    let history = new ResizeArray<string>()
    let mutable historyPointer = 0
    let mutable expectedPrompts = 0
    let mutable canScriptFileLoaded = true
    [<Literal>]
    let Prompt = "> "    

    [<DefaultValue>]
    val mutable setting : InteractiveOption
            
    let dataOrErrorhandler = 
        new DataReceivedEventHandler(fun _ e -> 
            outputQueue.Enqueue(e.Data)
            self.Dispatcher.BeginInvoke(Threading.DispatcherPriority.Send,Action(fun () -> self.ReadAll())) |> ignore)           
    do                      
        self.InitializeComponents()       
        self.BindCommands()
        self.AppendPrompt()
        self.SetSetting(InteractiveOption.Load())
        self.InitFSIProcess()        
        self.editor.TextArea.Focus() |> ignore
           
    member self.SetSetting(options : InteractiveOption ) =           
        self.setting <- options       
        self.editor.FontFamily <- getFont self.setting.EditorOption.FontName
        self.editor.FontSize <- self.setting.EditorOption.FontSize            
        self.editor.Options.IndentationSize <- int self.setting.EditorOption.IdentSize
        self.editor.WordWrap <- self.setting.EditorOption.WordWrap
        if self.setting.EditorOption.SyntaxHilighting then
            self.editor.SyntaxHighlighting <- Highlighting.HighlightingManager.Instance.GetDefinition("F#")
        else
            self.editor.SyntaxHighlighting <- null
        self.editor.Foreground <- new SolidColorBrush(self.setting.EditorOption.FontColor)                  
        self.editor.Background <- new SolidColorBrush(self.setting.EditorOption.BackgroundColor)          
        self.editor.ShowLineNumbers <- self.setting.EditorOption.ShowLineNumber
        self.editor.Options.ConvertTabsToSpaces <- self.setting.EditorOption.ConvertTabsToSpaces        
        self.editor.Options.ShowEndOfLine <- self.setting.EditorOption.ShowEndOfLine
        self.editor.Options.ShowSpaces <- self.setting.EditorOption.ShowSpace
        self.editor.Options.ShowTabs <- self.setting.EditorOption.ShowTabs
        self.setting.Save();
                      
    /// Show the command text in the editor and invoke it.    
    member self.InjectCommand (cmd:string) = 
        self.editor.AppendText(cmd)          
        self.HandleInput(Key.Return)
    
    let (|CustomCmdWithParam|_|) (customCmd:string) (cmd:string)  =
        match cmd.Split([|'\"'|], StringSplitOptions.RemoveEmptyEntries) with
        | [|a;b;";;"|] when a.StartsWith(customCmd) -> Some(customCmd,b)
        | _ -> None

    let (|QuitCommand|) (cmd:string) =
        if ["#quit;;";"#q;;";"#reset;;"] |> List.exists ((=) cmd) then true
        else false
    
    let (|ClearCommand|) (cmd:string) =
        if ["#cls;;" ; "#clear;;"] |> List.exists ((=) cmd) then true
        else false
                            
    member self.AcceptCommand (cmd:string) =        
        let mutable cmdText = cmd       
         
        let passToFsi (cmd:string) = 
            expectedPrompts <- expectedPrompts + 1
            fsiProcess.StandardInput.WriteLine(cmd)            
            CommandProcessStatus.Success

        let getBrush (colorStr : string) =            
            if isHex colorStr then
                Some (makeBrush colorStr)                
            else
                let p = typeof<Colors>.GetProperties() |> Array.tryPick (fun (x:PropertyInfo) -> if x.Name.ToLower() = colorStr.ToLower() then Some x else None)
                match p with
                | Some(a) -> 
                    let c = a.GetValue(null, null) :?> Color
                    Some(new SolidColorBrush(c))                    
                | _ -> None                    

        if cmd.Trim().EndsWith(";;", StringComparison.Ordinal) then
            if cmdText.StartsWith("#") then
                cmdText <- Regex.Replace(cmd, @"\s", String.Empty)
                match cmdText.ToLower() with
                | QuitCommand true -> 
                    self.RestartFSIProcess()
                    CommandProcessStatus.Success
                | ClearCommand true ->
                    self.ClearConsole()
                    CommandProcessStatus.Success
                | CustomCmdWithParam "#bgc" t -> 
                    let colorStr = snd t
                    match getBrush colorStr with
                    | Some(b) -> 
                        self.editor.Background <- b
                        CommandProcessStatus.Success
                    | _ -> CommandProcessStatus.Error
                | CustomCmdWithParam "#fgc" t -> 
                    let colorStr = snd t
                    match getBrush colorStr with
                    | Some(b) -> 
                        self.editor.Foreground <- b
                        CommandProcessStatus.Success
                    | _ -> CommandProcessStatus.Error      
                | CustomCmdWithParam "#fz" t ->
                    let size = snd t
                    match Double.TryParse(size) with
                    | true, r -> 
                        self.editor.FontSize <- r
                        CommandProcessStatus.Success
                    | _ -> CommandProcessStatus.Error
                | CustomCmdWithParam "#hl" t ->
                    let flag = snd t
                    match flag.Trim() with
                    | "on" -> 
                        self.editor.SyntaxHighlighting <- Highlighting.HighlightingManager.Instance.GetDefinition("F#")
                        CommandProcessStatus.Success
                    | "off" ->
                        self.editor.SyntaxHighlighting <- null
                        CommandProcessStatus.Success
                    | _ -> CommandProcessStatus.Error
                | _ -> passToFsi cmd
            else
                passToFsi cmd
        else CommandProcessStatus.InProgess
                  
    member private self.InitFSIProcess() =        
        let fsiPath = if String.IsNullOrWhiteSpace(self.setting.FSIPath) then InteractiveOption.Default.FSIPath                        
                      else self.setting.FSIPath                                  
        if File.Exists(fsiPath) then
            fsiProcess.StartInfo.FileName <- fsiPath
            fsiProcess.StartInfo.UseShellExecute <- false
            fsiProcess.StartInfo.CreateNoWindow <- true
            fsiProcess.StartInfo.RedirectStandardError <- true
            fsiProcess.StartInfo.RedirectStandardInput <- true
            fsiProcess.StartInfo.RedirectStandardOutput <- true
            fsiProcess.EnableRaisingEvents <- true;         
            fsiProcess.StartInfo.Arguments <- self.setting.FSIOptions               
            fsiProcess.OutputDataReceived.AddHandler(dataOrErrorhandler)
            fsiProcess.ErrorDataReceived.AddHandler(dataOrErrorhandler)                 
            fsiProcess.Start() |> ignore        
            fsiProcess.BeginErrorReadLine()
            fsiProcess.BeginOutputReadLine()
            canScriptFileLoaded <- true
        else           
            canScriptFileLoaded <- false
            self.editor.SyntaxHighlighting <- null             
            self.editor.Document.Insert(0,"Refunctor cannot locate " + self.setting.FSIPath  + " in your machine." + Environment.NewLine + 
                                        "Please locate fsi.exe in Options dialog." + Environment.NewLine +
                                        "Or please make sure to install fsharp. Please visit http://www.fsharp.net for more info. ")            
    
    member private self.RestartFSIProcess() =
        if not fsiProcess.HasExited then            
            fsiProcess.CancelErrorRead()
            fsiProcess.CancelOutputRead()
            fsiProcess.OutputDataReceived.RemoveHandler(dataOrErrorhandler)
            fsiProcess.ErrorDataReceived.RemoveHandler(dataOrErrorhandler)                                             
            fsiProcess.Kill()
            fsiProcess.WaitForExit()
        (* Beware Identation *)                    
        self.ClearConsole()
        self.InitFSIProcess()
    
    member private self.EndOffset 
        with get() = self.editor.TextArea.ReadOnlySectionProvider?EndOffset
        and set v = self.editor.TextArea.ReadOnlySectionProvider?EndOffset <- v

    member private self.ClearConsole() =
        self.editor.Document.Text <- String.Empty
        cleared <- true
        self.AppendPrompt()

    member private self.CommandText 
        with get() = self.editor.Document.GetText( new TextSegment(StartOffset = self.EndOffset, EndOffset = self.editor.Document.TextLength))
        and set v = self.editor.Document.Replace(new TextSegment(StartOffset = self.EndOffset, EndOffset = self.editor.Document.TextLength), v)

    member private self.HandleInput (key : Key) : bool =
        match key with
        | Key.Back
        | Key.Delete ->
            if self.editor.SelectionStart = 0  && self.editor.SelectionLength = self.editor.Document.TextLength then
                self.ClearConsole()
                true
            else false
        | Key.Down ->
           if Keyboard.Modifiers = ( ModifierKeys.Shift) then                 
                false
            else
                try                
                    historyPointer <- min (historyPointer + 1)  history.Count
                    if historyPointer = history.Count then
                        self.CommandText <- String.Empty
                    else
                        self.CommandText <- history.[historyPointer]
                    self.editor.ScrollToEnd()
                    true
                with |_ -> false // Sometimes IndexOutOfRangeException throws and ignore it.
        | Key.Up ->
            if Keyboard.Modifiers = (ModifierKeys.Shift) then                 
                false
            else
                try               
                    historyPointer <- max (historyPointer - 1) 0
                    if historyPointer = history.Count then
                        self.CommandText <- String.Empty
                    else
                        self.CommandText <- history.[historyPointer]
                    self.editor.ScrollToEnd()
                    true
                with |_ -> false // Sometimes IndexOutOfRangeException throws and ignore it.
        | Key.Return ->  
            if Keyboard.Modifiers = ModifierKeys.Shift then false
            else                                                                                                                  
                let cmdText = self.CommandText
                cleared <- false
                let document = self.editor.Document
                match self.AcceptCommand(cmdText) with
                | CommandProcessStatus.Success ->                                                        
                    if not cleared then 
                        if not (document.GetCharAt(document.TextLength - 1) = '\n') then
                            document.Insert(document.TextLength,Environment.NewLine)                    
                        self.AppendPrompt()                    
                        self.editor.Select(document.TextLength, 0)
                    else
                        self.CommandText <- String.Empty
                    cleared <- false
                    history.Add(cmdText.Trim())
                    historyPointer <- history.Count
                    self.editor.ScrollToEnd()
                    true
                | CommandProcessStatus.Error -> 
                    self.editor.AppendText("\n\n--> Invalid command or parameter!\n\n")
                    self.AppendPrompt()                    
                    self.editor.Select(document.TextLength - 1, 0)
                    history.Add(cmdText.Trim())
                    historyPointer <- history.Count
                    self.editor.ScrollToEnd()
                    false
                | _ -> false
        | _ -> false

    member private self.ReadAll() =
        let sb = new StringBuilder()
        let appendLine = sb.AppendLine >> ignore
        while outputQueue.Count > 0 do
            let _, v = outputQueue.TryDequeue()
            appendLine v
        let offset = ref 0
        for i = 0 to expectedPrompts do
            if (!offset + 1) < sb.Length && sb.[!offset] = '>' && sb.[!offset + 1] = ' ' then
                offset := !offset + 2                    
        expectedPrompts <- 0                
        let text = sb.ToString(!offset, sb.Length - !offset)                                           
        self.editor.Document.Insert(self.EndOffset - Prompt.Length, text)
        self.editor.ScrollToEnd()
        self.EndOffset <- self.EndOffset + text.Length    

    member private self.AppendPrompt() =
        self.editor.AppendText(Prompt)
        self.SetReadonly()
        self.editor.Document.UndoStack.ClearAll()
    
    member private self.SetReadonly() =
        self.editor.TextArea.ReadOnlySectionProvider?EndOffset <- self.editor.Document.TextLength

    member private self.InitializeComponents() =
        let toolItems : InvokerItemType<_> array =
            [| 
                CommandItem("InteractiveSave", "Save (Ctrl+S)", ApplicationCommands.Save)    
                CommandItem("Load Script File", "Load Script File", RefunctorCommands.LoadScriptFileCommand)           
                InvokerItemType.Separator
                CommandItem("Cut", "Cut (Ctrl+X)", ApplicationCommands.Cut)
                CommandItem("Copy", "Copy (Ctrl+C)", ApplicationCommands.Copy)
                CommandItem("Paste", "Paste (Ctrl+V)", ApplicationCommands.Paste)
                CommandItem("Delete", "Delete (Del)", ApplicationCommands.Delete)
                InvokerItemType.Separator
                CommandItem("Undo", "Undo (Ctrl+Z)", ApplicationCommands.Undo)
                CommandItem("Redo", "Redo (Ctrl+Y)", ApplicationCommands.Redo)
                InvokerItemType.Separator                
                CommandItem("Clear All", "Clear All", RefunctorCommands.ClearAllCommand)
                CommandItem("Clear History", "Clear History", RefunctorCommands.ClearHistoryCommand)
                CommandItem("Reset Session", "Reset Session", RefunctorCommands.ResetSessionCommand)     
                InvokerItemType.Separator
                CommandItem("Options", "Options", RefunctorCommands.OptionsCommand)           
            |] 
        let contextMenuItems : InvokerItemType<_> array =
            [| 
                CommandItem("Reset Session", String.Empty, RefunctorCommands.ResetSessionCommand)                      
                InvokerItemType.Separator
                CommandItem("Cut", "Ctrl+X", ApplicationCommands.Cut)
                CommandItem("Copy", "Ctrl+C", ApplicationCommands.Copy)
                CommandItem("Paste", "Ctrl+V", ApplicationCommands.Paste)                
                InvokerItemType.Separator
                CommandItem("Undo", "Ctrl+Z", ApplicationCommands.Undo)
                CommandItem("Redo", "Ctrl+Y", ApplicationCommands.Redo)
                InvokerItemType.Separator                
                CommandItem("Clear All", String.Empty, RefunctorCommands.ClearAllCommand)
                CommandItem("Clear History", String.Empty, RefunctorCommands.ClearHistoryCommand)                             
            |]                        
        self.toolbar <- buildToolbar toolItems        
        self.editor <- getEditor()
        let border = new Border(BorderBrush = Brushes.Gray, BorderThickness = new Thickness(1.7), Margin = Thickness(4.,0.,4.,0.), CornerRadius = CornerRadius(3.)) 
        Panel.SetZIndex(border,Int32.MaxValue)
        border.Child <- self.editor
        self.editor.ContextMenu <- buildContextMenu contextMenuItems
        self.editor.TextArea.PreviewKeyDown.Add ( fun e -> e.Handled <- self.HandleInput(e.Key))
        let grid = new Grid()
        grid.RowDefinitions.Add(RowDefinition(Height = GridLength.Auto))
        grid.RowDefinitions.Add(RowDefinition(Height = GridLength(1.,GridUnitType.Star)))
        Grid.SetRow(self.toolbar,0)
        Grid.SetRow(border,1)                        
        grid.Children.Add(self.toolbar)  |> ignore
        grid.Children.Add(border)   |> ignore            
        self.Content <- grid 
    
    let makeCommandBinding (command:#ICommand, execute :ExecutedRoutedEventHandler, canExecute : CanExecuteRoutedEventHandler) =
        self.CommandBindings.Add(new CommandBinding(command, execute, canExecute)) |> ignore
        
    member private self.BindCommands () =
        (* CommandBinding For SaveCommand *)
        makeCommandBinding
            (ApplicationCommands.Save,
            ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs ) ->  
                    let dialog = new SaveFileDialog()
                    dialog.DefaultExt <- ".txt"
                    dialog.Filter <- "Text File |*.txt" 
                    dialog.FileName <- "FSharp"
                    dialog.RestoreDirectory <- true                        
                    let r = dialog.ShowDialog()
                    if r.HasValue && r.Value then    
                        try                     
                            use f = new FileStream(dialog.FileName, FileMode.Create)
                            use writer = new StreamWriter(f)
                            writer.Write(self.editor.Text)
                            writer.Flush()                                                         
                        with
                        | _ -> showError("Sorry, there was an error saving the file.\nPlease try again.")),
            CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs ) -> e.CanExecute <- self.editor.Text.Length > 0))

        (* CommandBinding For ClearAllCommand*)                 
        makeCommandBinding(RefunctorCommands.ClearAllCommand,                
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> self.ClearConsole()),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- true )) 

        (* CommandBinding For ResetSessionCommand *)
        makeCommandBinding(RefunctorCommands.ResetSessionCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> self.RestartFSIProcess()),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- true )) 

        (* CommandBinding For LoadScriptFileCommand *)
        makeCommandBinding(RefunctorCommands.LoadScriptFileCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> 
                    let dialog = new OpenFileDialog()
                    dialog.DefaultExt <- ".fsx"
                    dialog.Filter <- "F# Script File |*.fsx|F# Source File |*.fs"
                    dialog.RestoreDirectory <- true
                    let r = dialog.ShowDialog()
                    if r.HasValue && r.Value  then
                        self.AcceptCommand("#load @\"" + dialog.FileName + "\";;") |> ignore
                    ),              
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- canScriptFileLoaded))

        (* CommandBinding For ClearHistoryCommand *)
        makeCommandBinding(RefunctorCommands.ClearHistoryCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> history.Clear()),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- history.Count > 0 )) 

        (* CommandBinding For OptionsCommand *)
        makeCommandBinding(RefunctorCommands.OptionsCommand,
                ExecutedRoutedEventHandler(fun s (e:ExecutedRoutedEventArgs) -> 
                    let dialog = new InteractiveOptionDialog(self.setting)   
                    dialog.ShowDialog() |> ignore
                    match dialog.DialogResult with
                    | DialogResult.OK -> 
                        self.SetSetting(dialog.Settings)                        
                    | DialogResult.Reset -> 
                        self.SetSetting(InteractiveOption.Default)                                          
                    | _ -> ()
                    ),                
                CanExecuteRoutedEventHandler(fun s (e:CanExecuteRoutedEventArgs) -> e.CanExecute <- true )) 